# AssociationTableNamingStrategy.java

    package org.codefilarete.stalactite.dsl.naming;

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    import java.util.TreeSet;

    import org.codefilarete.reflection.AccessorDefinition;
    import org.codefilarete.stalactite.dsl.MappingConfigurationException;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.PrimaryKey;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.tool.Strings;

    /**
     * Contract for giving a name to an association table (one-to-many cases)
     *
     * @author Guillaume Mary
     */
    public interface AssociationTableNamingStrategy {

    	/**
    	 * Gives association table name
    	 * @param accessorDefinition a representation of the method (getter or setter) that gives the collection to be persisted,
    	 * given to contextualize method call and help to decide which naming to apply
    	 * @param source columns that maps "one" side (on source table)
    	 * @param target columns that maps "many" side (on target table)
    	 * @return table name for association table
    	 */
    	<LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>>
    	String giveName(AccessorDefinition accessorDefinition, PrimaryKey<LEFTTABLE, ?> source, PrimaryKey<RIGHTTABLE, ?> target);

    	/**
    	 * Gives column names referenced by given keys
    	 *
    	 * @param accessorDefinition a representation of the method (getter or setter) that gives the collection to be persisted,
    	 * given to contextualize method call and help to decide which naming to apply
    	 * @param leftPrimaryKey primary key of left-side-entity table
    	 * @param rightPrimaryKey primary key of right-side-entity table
    	 * @return a mapping between each left and right primary key columns and the names of the ones that should be created in association table
    	 */
    	<LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>, LEFTID, RIGHTID> ReferencedColumnNames<LEFTTABLE, RIGHTTABLE>
    	giveColumnNames(AccessorDefinition accessorDefinition, PrimaryKey<LEFTTABLE, LEFTID> leftPrimaryKey, PrimaryKey<RIGHTTABLE, RIGHTID> rightPrimaryKey);

    	/**
    	 * Small structure that stores column names of association with their matching exported key column in left and right tables
    	 *
    	 * @param <LEFTTABLE> left table type
    	 * @param <RIGHTTABLE> right table type
    	 */
    	class ReferencedColumnNames<LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>> {

    		private final Map<Column<LEFTTABLE, Object>, String> leftColumnNames = new HashMap<>();

    		private final Map<Column<RIGHTTABLE, Object>, String> rightColumnNames = new HashMap<>();

    		/**
    		 * Set left column name in association table that matches given left-table column
    		 * @param column the column coming from left table
    		 * @param name name of association table column for given left-table column
    		 */
    		public void setLeftColumnName(Column<LEFTTABLE, ?> column, String name) {
    			this.leftColumnNames.put((Column<LEFTTABLE, Object>) column, name);
    		}

    		/**
    		 * Give left column name in association table that matches given left-table column
    		 * @param column the left-table column
    		 */
    		public String getLeftColumnName(Column<LEFTTABLE, ?> column) {
    			return leftColumnNames.get(column);
    		}

    		/**
    		 * Exposes all column references to simplify column name modification : without this access the user has to use {@link #getLeftColumnName(Column)}
    		 * which implies to have the {@link Column} object, this is cumbersome because it means the user must have declared its {@link Table}.
    		 * @return all column references
    		 */
    		public Map<Column<LEFTTABLE, Object>, String> getLeftColumnNames() {
    			return leftColumnNames;
    		}

    		/**
    		 * Set right column name in association table that matches given right-table column
    		 * @param column the column coming from left table
    		 * @param name name of association table column for given left-table column
    		 */
    		public void setRightColumnName(Column<RIGHTTABLE, ?> column, String name) {
    			this.rightColumnNames.put((Column<RIGHTTABLE, Object>) column, name);
    		}

    		/**
    		 * Give right column name in association table that matches given right-table column
    		 * @param column the right-table column
    		 */
    		public String getRightColumnName(Column<RIGHTTABLE, ?> column) {
    			return rightColumnNames.get(column);
    		}

    		/**
    		 * Exposes all column references to simplify column name modification : without this access the user has to use {@link #getRightColumnName(Column)}
    		 * which implies to have the {@link Column} object, this is cumbersome because it means the user must have declared its {@link Table}.
    		 * @return all column references
    		 */
    		public Map<Column<RIGHTTABLE, Object>, String> getRightColumnNames() {
    			return rightColumnNames;
    		}
    	}

    	AssociationTableNamingStrategy DEFAULT = new DefaultAssociationTableNamingStrategy();

    	AssociationTableNamingStrategy HIBERNATE = new HibernateAssociationTableNamingStrategy();

    	/**
    	 * Default implementation of the {@link AssociationTableNamingStrategy} interface.
    	 * Will use relation property name for it, prefixed with source table name.
    	 * For instance: for a Country entity with the getCities() getter to retrieve country cities,
    	 * the association table will be named "Country_cities".
    	 * If property cannot be deduced from getter (it doesn't start with "get") then target table name will be used, suffixed by "s".
    	 * For instance: for a Country entity with the giveCities() getter to retrieve country cities,
    	 * the association table will be named "Country_Citys".
    	 */
    	class DefaultAssociationTableNamingStrategy implements AssociationTableNamingStrategy {

    		@Override
    		public <LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>>
    		String giveName(AccessorDefinition accessor, PrimaryKey<LEFTTABLE, ?> source, PrimaryKey<RIGHTTABLE, ?> target) {
    			return accessor.getDeclaringClass().getSimpleName() + "_" + accessor.getName().replace('.', '_');
    		}

    		@Override
    		public <LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>, LEFTID, RIGHTID>
    		ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> giveColumnNames(AccessorDefinition accessorDefinition,
    																	 PrimaryKey<LEFTTABLE, LEFTID> leftPrimaryKey,
    																	 PrimaryKey<RIGHTTABLE, RIGHTID> rightPrimaryKey) {

    			ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> result = new ReferencedColumnNames<>();

    			// columns pointing to left table get same names as original ones
    			leftPrimaryKey.getColumns().forEach(column -> {
    				String leftColumnName = Strings.uncapitalize(leftPrimaryKey.getTable().getName()) + "_" + column.getName();
    				result.setLeftColumnName(column, leftColumnName);
    			});
    			Set<String> existingColumns = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
    			existingColumns.addAll(result.leftColumnNames.values());

    			// columns pointing to right table get a name that contains accessor definition name
    			String rightSideColumnNamePrefix = accessorDefinition.getName().replace('.', '_');
    			rightPrimaryKey.getColumns().forEach(column -> {
    				String rightColumnName = Strings.uncapitalize(rightSideColumnNamePrefix + "_" + column.getName());
    				if (existingColumns.contains(rightColumnName)) {
    					throw new MappingConfigurationException("Identical column names in association table of collection "
    							+ accessorDefinition + " : " + rightColumnName);
    				}
    				result.setRightColumnName(column, rightColumnName);
    			});
    			return result;
    		}
    	}

    	/**
    	 * Hibernate implementation of the {@link AssociationTableNamingStrategy} interface.
    	 * The table name is made of left table one and right table one, separated by an underscore.
    	 * @author Guillaume Mary
    	 */
    	class HibernateAssociationTableNamingStrategy implements AssociationTableNamingStrategy {

    		@Override
    		public <LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>>
    		String giveName(AccessorDefinition accessor, PrimaryKey<LEFTTABLE, ?> source, PrimaryKey<RIGHTTABLE, ?> target) {
    			return source.getTable().getName() + "_" + target.getTable().getName();
    		}

    		@Override
    		public <LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>, LEFTID, RIGHTID>
    		ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> giveColumnNames(AccessorDefinition accessor,
    																	 PrimaryKey<LEFTTABLE, LEFTID> leftPrimaryKey,
    																	 PrimaryKey<RIGHTTABLE, RIGHTID> rightPrimaryKey) {

    			ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> result = new ReferencedColumnNames<>();

    			// columns pointing to left table get same names as original ones
    			leftPrimaryKey.getColumns().forEach(column -> {
    				String leftColumnName = Strings.uncapitalize(leftPrimaryKey.getTable().getName()) + "_" + column.getName();
    				result.setLeftColumnName(column, leftColumnName);
    			});
    			Set<String> existingColumns = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
    			existingColumns.addAll(result.getLeftColumnNames().values());

    			// columns pointing to right table get a name that contains accessor definition name
    			rightPrimaryKey.getColumns().forEach(column -> {
    				String rightColumnName = column.getName();
    				if (existingColumns.contains(rightColumnName)) {
    					throw new MappingConfigurationException("Identical column names in association table of collection "
    							+ accessor + " : " + rightColumnName);
    				}
    				result.setRightColumnName(column, rightColumnName);
    			});
    			return result;
    		}
    	}
    }